﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.AccessControl;
using System.Web;
using Microsoft.Ajax.Utilities;
using System.Web.Hosting;

namespace SimpleMin
{
    /// <summary>
    /// SimpleMin, a simple efficient class for automating js and css bundling within a .NET web application. 
    /// Depends on Microsoft AjaxMin compression library.
    /// Oliver Grimes, 17/09/2012.
    /// </summary>
    public static class SimpleMinifier
    {
        /// <summary>
        /// Cache the current version number to identify when to re-bundle.
        /// </summary>
        private static string _version;
        /// <summary>
        /// Cache our raw HTML reference paths for debugging.
        /// </summary>
        private static string _references;
        /// <summary>
        /// Cache our bundled HTML references.
        /// </summary>
        private static string _bundleReferences;
        /// <summary>
        /// Reference key for minified files.
        /// </summary>
        private const string _bundleKey = "-bundle.";
        /// <summary>
        /// HTML script reference template.
        /// </summary>
        private const string _jsReferenceTemplate = "<script type=\"text/javascript\" src=\"{0}\"></script>\r\n";
        /// <summary>
        /// HTML stylesheet reference template
        /// </summary>
        private const string _cssReferenceTemplate = "<link type=\"text/css\" rel=\"stylesheet\" href=\"{0}\"></script>\r\n";
        /// <summary>
        /// Used to parse the relative directories.
        /// </summary>
        private const char _directorySeparator = '/';

        /// <summary>
        /// Performs bundling, removal of old bundles and reference path caching.
        /// </summary>
        /// <param name="refs">Reference paths to bundle, will generate one bundle per directory, per type (js/css).</param>
        private static void Bundle(IEnumerable<string> refs, HttpServerUtility server, bool orderAlpha)
        {
            string tempReferencePaths = "", tempBundleContent = "";
            Minifier minifier = new Minifier();

            CodeSettings jsSettings = new CodeSettings()
            {
                PreserveImportantComments = false,
                RemoveUnneededCode = false,
                TermSemicolons = true,
                MacSafariQuirks = true,
                EvalTreatment = EvalTreatment.Ignore,
                LocalRenaming = LocalRenaming.KeepAll
            };

            CssSettings cssSettings = new CssSettings();

            //Get all of our distinct reference directories
            var bundleGroups = refs
                .GroupBy(r => new
                {
                    directory = ExtractDirectory(r),
                    extension = r.Split('.').Last()
                }).OrderBy(g => g.Key.extension); //Order to ensure css renders first

            //Create bundles that don't exist
            foreach (var bundle in bundleGroups)
            {
                //Determine expected bundle path
                string createPath = Path.Combine
                    (server.MapPath(bundle.Key.directory), _version + _bundleKey + bundle.Key.extension);

                //Determine web relative reference path
                string referencePath = bundle.Key.directory + _directorySeparator + _version + _bundleKey + bundle.Key.extension;

                //Add generated bundle reference to cache
                tempReferencePaths += GenerateScriptReference(referencePath, bundle.Key.extension);

                //Create bundle if not existing
                if (!File.Exists(createPath))
                {
                    var orderedBundle = bundle.Select(b => b);
                    if (orderAlpha)
                        orderedBundle = orderedBundle.OrderBy(b => b);

                    //Minify the content of each bundle source file
                    //Order by file name so that user can enforce minifiy order
                    foreach (var file in orderedBundle)
                    {
                        string source = "";
                        string path = "";

                        path = server.MapPath(file);

                        //Extract the css/js from the source file
                        using (var inputFile = new StreamReader(path))
                            source = inputFile.ReadToEnd();

                        //Add js/css to bundle content
                        switch (bundle.Key.extension)
                        {
                            case "js":
                                tempBundleContent += minifier.MinifyJavaScript(source, jsSettings);
                                break;
                            case "css":
                                tempBundleContent += minifier.MinifyStyleSheet(source, cssSettings);
                                break;
                            default:
                                break;
                        }
                    }

                    //Generate the bundle file
                    char[] buffer = tempBundleContent.ToCharArray();

                    //If source path is virtualised, multiple writes may be occuring
                    try
                    {
                        //Create file
                        using (FileStream stream = new FileStream(createPath, FileMode.CreateNew, FileSystemRights.CreateFiles,
                                                                    FileShare.Write, buffer.Length, FileOptions.None))
                        {
                            //Write minified source
                            using (StreamWriter writer = new StreamWriter(stream))
                            {
                                writer.AutoFlush = true;
                                writer.Write(buffer);
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        //Exception creating file
                    }

                    //Reset for the next bundle
                    tempBundleContent = string.Empty;
                    Array.Clear(buffer, 0, buffer.Length);
                }
            }

            //Overwrite our local cache
            _bundleReferences = tempReferencePaths;

            //Delete any out-dated bundles
            foreach (var bundle in bundleGroups)
            {
                //Delete any existing bundles
                foreach (string path in Directory.EnumerateFiles(server.MapPath(bundle.Key.directory)))
                {
                    //Delete files that match the bundle signature but don't match the current version
                    if (path.EndsWith(_bundleKey + bundle.Key.extension) && !path.Contains(_version + _bundleKey))
                        File.Delete(path);
                }
            }
        }

        /// <summary>
        /// Generate HTML file reference.
        /// </summary>
        /// <param name="source"></param>
        /// <returns></returns>
        private static string GenerateScriptReference(string source, string type)
        {
            switch (type)
            {
                case "js":
                    return string.Format(_jsReferenceTemplate, source);
                case "css":
                    return string.Format(_cssReferenceTemplate, source);
                default:
                    return string.Format(_jsReferenceTemplate, source);
            }
        }
        /// <summary>
        /// Extracts the directory element of a relative path.
        /// </summary>
        /// <param name="relativePath"></param>
        /// <returns></returns>
        private static string ExtractDirectory(string relativePath)
        {
            string[] dirs = relativePath.Split(_directorySeparator);
            return string.Join(_directorySeparator.ToString(), dirs, 0, dirs.Length - 1);
        }

        /// <summary>
        /// Populate list of all .js and .css files to be minified.
        /// </summary>
        /// <param name="references"></param>
        /// <param name="version">Unique identifier.  When changed, causes re-bundling of all files.</param>
        public static void LoadBundles(IEnumerable<string> references, string version, HttpServerUtility server, bool orderAlpha = true)
        {
            //Cache required properties
            _version = version;

            //Bundle if required, or if version has changed
            Bundle(references, server, orderAlpha);

            //Debug references
            var refs = references.Select(r => GenerateScriptReference(r, r.Split('.').Last()));

            if (orderAlpha)
                refs = refs.OrderBy(r => r);
            _references = string.Join("", refs);
        }
        /// <summary>
        /// Render js/css reference paths.  If application is in debug mode, uncompressed files are referenced.
        /// </summary>
        /// <returns></returns>
        public static string BundleLinks()
        {
            //Load the raw references for the development env
            if (HttpContext.Current.IsDebuggingEnabled)
                return _references;
            return _bundleReferences;
        }
    }
}
